﻿Imports System.Linq.Expressions
Imports System.Reflection
Imports System.Collections.ObjectModel
Imports Kairyu.Connection

'Imports System.Collections.ObjectModel
'Imports System.Collections.Specialized

'http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx

Namespace Load

    ''' <summary>
    ''' クエリプロバイダー
    ''' </summary>
    ''' <remarks></remarks>
    Public MustInherit Class QueryProviderBase
        Implements IQueryProvider

        Protected Sub New()
        End Sub

#Region "プロパティ"
        Protected Friend _loader As ModelLoader
        ''' <summary>
        ''' モデルローダー
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        ReadOnly Property Loader As ModelLoader
            Get
                Return _loader
            End Get
        End Property
#End Region

#Region "IQueryProvider"
        Private Function IQueryProvider_CreateQuery(Of S)(expression As Expression) As IQueryable(Of S) Implements IQueryProvider.CreateQuery
            Return New Query(Of S)(Me, expression)
        End Function

        Private Function IQueryProvider_CreateQuery(expression As Expression) As IQueryable Implements IQueryProvider.CreateQuery
            Dim elementType As Type = TypeSystem.GetElementType(expression.Type)
            Try
                Return DirectCast(Activator.CreateInstance(GetType(Query(Of )).MakeGenericType(elementType), New Object() {Me, expression}), IQueryable)
            Catch ex As TargetInvocationException
                Throw ex.InnerException
            End Try
        End Function

        Private Function IQueryProvider_Execute(Of S)(expression As Expression) As S Implements IQueryProvider.Execute
            Return DirectCast(Me.Execute(expression), S)
        End Function

        Private Function IQueryProvider_Execute(expression As Expression) As Object Implements IQueryProvider.Execute
            Return Me.Execute(expression)
        End Function
#End Region

        ''' <summary>
        ''' 実行します
        ''' </summary>
        ''' <param name="e"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Overridable Function Execute(e As ConstantExpression) As Object
            Dim t As Type = e.Type.GetGenericArguments.First
            Dim q As IQuery = DirectCast(e.Value, IQuery)
            Dim logs As New Dictionary(Of String, Long)

            Dim totalSw As New Stopwatch
            totalSw.Start()

            Dim sw As New Stopwatch
            sw.Start()

            '定義
            Dim rt As RootTableAlias = Me.CreateRootTableAlias(t, q)

            sw.Stop()
            logs.Add("definition", sw.ElapsedMilliseconds)
            sw.Restart()

            Dim lst As IList = Me.CreateGenericList(t)

            'DBコマンド
            Using cmd As IDbCommand = Me.CreateCommand(rt, q)
                sw.Stop()
                logs.Add("command", sw.ElapsedMilliseconds)
                sw.Restart()

                '読み込み
                Dim rows As Integer = 0
                Using r As IDataReader = cmd.ExecuteReaderWithLog(rt.Table.LoadTableName)
                    sw.Stop()
                    logs.Add("query", sw.ElapsedMilliseconds)
                    sw.Restart()

                    While r.Read()
                        lst.Add(Me.CreateModel(rt, r))
                        rows += 1
                    End While

                    sw.Stop()
                    totalSw.Stop()
                    logs.Add("mapping", sw.ElapsedMilliseconds)
                End Using

                Log.DoFinishAction(Me, totalSw.ElapsedMilliseconds, rows, logs)
            End Using

            '追加処理
            Me.OnLoaded(lst, t, q)

            '子リストに対し、Lazy処理を仕込む
            '#104 子結合条件が複数の場合、Fetchができない
            For Each prop As PropertyInfo In (From el In t.ToTable.ChildRelations Select el.CollectionProperty Distinct)
                Dim p As PropertyInfo = prop
                Dim rels As IEnumerable(Of ChildRelation) = (From el In prop.DeclaringType.ToTable.ChildRelations Where el.CollectionProperty.Equals(p)).ToList
                Dim childT = rels.First.CollectionProperty.PropertyType.GetGenericArguments.First
                Dim meth = Me.GetType.GetMethod("InitLazyList")

                meth.MakeGenericMethod({t, childT}).Invoke(Me, {lst, rels})
            Next

            '設定を初期値に戻します
            Me.Loader.ResetQueryType()

            Return lst
        End Function

        ''' <summary>
        ''' DBコマンドを作成します
        ''' </summary>
        ''' <param name="rt"></param>
        ''' <param name="q"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Protected Friend Function CreateCommand(rt As RootTableAlias, q As IQuery) As IDbCommand
            Dim cmd As IDbCommand = rt.CreateCommand(Me.Loader.Connection, q)
            cmd.Transaction = Me.Loader.Transaction
            Return cmd
        End Function

        ''' <summary>
        ''' 読み込み定義を作成します
        ''' </summary>
        ''' <param name="t"></param>
        ''' <param name="q"></param>
        ''' <returns></returns>
        Protected Friend Overridable Function CreateRootTableAlias(t As Type, q As IQuery) As RootTableAlias
            Dim rt As New RootTableAlias
            rt.ActualTableAlias = rt

            'クラスとテーブルの紐付け
            rt.Table = t.ToTable
            rt.IgnoreProperty = Nothing

            Return rt
        End Function

        ''' <summary>
        ''' 読み込み完了後イベント
        ''' </summary>
        ''' <param name="lst"></param>
        ''' <param name="t"></param>
        ''' <param name="q"></param>
        ''' <remarks></remarks>
        Protected Friend MustOverride Sub OnLoaded(lst As IList, t As Type, q As IQuery)

        ''' <summary>
        ''' ジェネリックリストを新規作成する
        ''' </summary>
        ''' <param name="t"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Private Function CreateGenericList(t As Type) As IList
            Dim geneType As Type = GetType(List(Of )).MakeGenericType({t})
            Return Activator.CreateInstance(geneType)
        End Function

        ''' <summary>
        ''' 子格納コレクションをLazyListで初期化する
        ''' </summary>
        ''' <typeparam name="MyT">親（自身）の型</typeparam>
        ''' <typeparam name="ChildT">子の型</typeparam>
        ''' <param name="lst">親のリスト</param>
        ''' <param name="rels">親と子を結合する条件</param>
        ''' <remarks>
        ''' #104 子結合条件が複数の場合、Fetchができない
        ''' </remarks>
        Public Sub InitLazyList(Of MyT, ChildT)(lst As IList, rels As IEnumerable(Of ChildRelation))
            For Each instance As MyT In lst
                Dim ownerList As New QueryableList(Of MyT)(Me.Loader, New List(Of MyT)({instance}))
                Dim q As Query(Of ChildT) = Me.CreateChildQuery(Of MyT, ChildT)(ownerList, rels).MappingIgnore(rels.First.YourColumn.PropertyInfo)

                CollectionInitializer.InitAsLasy(instance, rels, q)
            Next
        End Sub

        ' ''' <summary>
        ' ''' 子格納コレクションをLazyListで初期化する
        ' ''' </summary>
        ' ''' <typeparam name="MyT">親（自身）の型</typeparam>
        ' ''' <typeparam name="ChildT">子の型</typeparam>
        ' ''' <param name="lst">親のリスト</param>
        ' ''' <param name="rel">親と子を結合する条件</param>
        ' ''' <remarks></remarks>
        '<Obsolete()>
        'Public Sub InitLazyList(Of MyT, ChildT)(lst As IList, rel As ChildRelation) ', t1 As MyT, t2 As ChildT
        '    For Each instance As MyT In lst
        '        Dim ownerList As New QueryableList(Of MyT)(Me.Loader, New List(Of MyT)({instance}))
        '        Dim q As Query(Of ChildT) = Me.CreateChildQuery(Of MyT, ChildT)(ownerList, {rel}).MappingIgnore(rel.YourColumn.PropertyInfo)

        '        CollectionInitializer.InitAsLasy(instance, q)
        '    Next
        'End Sub

        ''' <summary>
        ''' 子を読み込むクエリー式の作成
        ''' </summary>
        ''' <typeparam name="MyT">親（自身）の型</typeparam>
        ''' <typeparam name="ChildT">子の型</typeparam>
        ''' <param name="source"></param>
        ''' <param name="rels">親と子を結合する条件</param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Friend Function CreateChildQuery(Of MyT, ChildT)(source As QueryableList(Of MyT), rels As IEnumerable(Of ChildRelation)) As Query(Of ChildT)
            Dim q As Query(Of ChildT) = source.From(Of ChildT)()

            If rels.Count = 1 Then
                '単一の場合
                Dim rel As ChildRelation = rels.First
                q.WhereInOrEqual(rel.YourColumn.PropertyInfo, (From el In source.InnerList Select rel.MyColumn.GetValue(el) Distinct).ToArray)
            Else
                '複数の場合
                Dim fn As Func(Of MyT, Query(Of ChildT)) =
                    Function(x)
                        Dim subq As Query(Of ChildT) = source.From(Of ChildT)()
                        For Each rel As ChildRelation In rels
                            subq.WhereEquals(rel.MyColumn.PropertyInfo, rel.MyColumn.GetValue(x))
                        Next
                        Return subq
                    End Function

                q.WhereOR((From el In source.InnerList Select fn.Invoke(el)).ToArray)
            End If

            Return q
        End Function

#Region "Relation Object Mapping"

        ''' <summary>
        ''' モデルを生成します
        ''' </summary>
        ''' <param name="rt"></param>
        ''' <param name="r"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Function CreateModel(rt As RootTableAlias, ByVal r As IDataRecord) As Object
            '行辞書を初期化
            Dim row = New Dictionary(Of String, Object)
            For i As Integer = 0 To r.FieldCount - 1
                row.Add(r.GetName(i), r.GetValue(i))
            Next

            'インスタンスを生成します
            Return Me.CreateModel(rt, row)
        End Function

        ''' <summary>
        ''' 行アイテム辞書とテーブルエイリアスから、オブジェクトを作成します
        ''' </summary>
        ''' <remarks></remarks>
        Private Function CreateModel(als As TableAlias, row As Dictionary(Of String, Object)) As Object
            Try
                'ユニークキーの取得
                Dim actual As TableAlias = als.ActualTableAlias
                Dim key As String = ModelKeyFactory.Create(actual.Table.ModelType, (From el In actual.ColumnAliases Where el.Column.IsLoadKey Order By el.Column.PropertyInfo.Name Select el.ColumnAliasName), row)

                'キーがない（＝インスタンスしても全ての項目が空にしからない）場合、処理を中止
                If String.IsNullOrEmpty(key) Then Return Nothing

                'キャッシュ取得
                Dim m As Object = Nothing
                If Me.Loader.KeyDictionary.ContainsKey(key) Then
                    Dim cache As ModelCache = Me.Loader.KeyDictionary(key)

                    'キャッシュ値のマッピングが完了している場合はそのまま終了
                    If cache.IsMappingComplete Then Return cache.Model

                    '完了していない場合、不足分のマッピングを行うため継続
                    m = cache.Model
                End If

                '初期化
                If m Is Nothing Then m = actual.Table.ModelType.GetConstructor(Type.EmptyTypes).Invoke(Nothing)

                'マッピング
                Me.DoPropertyMapping(als, m, row)

                'モデルをキャッシュに追加（基底リレーションが存在する場合、マッピング完了としません）
                If Me.Loader.KeyDictionary.ContainsKey(key) Then Me.Loader.KeyDictionary.Remove(key)
                Me.Loader.KeyDictionary.Add(key, New ModelCache With {.IsMappingComplete = If(actual.Table.SuperRelations.Any, False, True), .Model = m})

                If als.RelationTableAliases.Any Then
                    '上流テーブルのマッピング（再帰）
                    For Each branch As RelationTableAlias In als.RelationTableAliases
                        Dim parent As Object = Me.CreateModel(branch, row)
                        branch.Relations.First.Join(m, parent)
                    Next
                Else
                    '最上層の場合、親プロパティをキャッシュ値と差し替え、インスタンスを共有
                    For Each item As PropertyInfo In (From el In actual.Table.ModelType.ToTable.Columns Where el.IsParent Select el.PropertyInfo Distinct)
                        Dim parent As Object = item.GetValue(m, Nothing)
                        Dim k As String = ModelKeyFactory.Create(parent)

                        If String.IsNullOrEmpty(k) Then Continue For

                        If Me.Loader.KeyDictionary.ContainsKey(k) Then
                            item.SetValue(m, Me.Loader.KeyDictionary(k).Model, Nothing)
                        Else
                            Me.Loader.KeyDictionary.Add(k, New ModelCache With {.IsMappingComplete = False, .Model = parent})
                        End If
                    Next
                End If

                Return m
            Catch ex As Exception
                Throw New InvalidOperationException("モデルの生成に失敗しました。", ex)
            End Try
        End Function

        ''' <summary>
        ''' プロパティマッピングを行います
        ''' </summary>
        ''' <param name="def"></param>
        ''' <remarks></remarks>
        Private Sub DoPropertyMapping(ByVal def As TableAlias, m As Object, row As Dictionary(Of String, Object))
            Dim c As ColumnAlias = Nothing
            Try
                For Each item As ColumnAlias In def.ColumnAliases
                    c = item
                    Dim val As Object = row.Item(c.ColumnAliasName)
                    c.Column.SetValue(m, val)
                Next
            Catch ex As Exception
                If c IsNot Nothing Then
                    Dim msg As String = String.Format("プロパティマッピングに失敗しました。（型：{0}、プロパティ：{1}、列：{2}）", m.GetType.Name, c.Column.PropertyInfo.Name, c.Index)
                    Throw New InvalidOperationException(msg, ex)
                Else
                    Throw
                End If
            End Try
        End Sub
#End Region

    End Class

End Namespace